# -*- coding: utf-8 -*-
import os
import random
import numpy
import pandas as pd
from csv import DictWriter
from deap import base, creator, tools
from timeit import default_timer as timer
import multiprocessing

df_points = pd.read_csv('data/meirong/sample_points.csv')
df_times = pd.read_csv('data/meirong/travel_times.csv')
small_constant = 0.000001

# Create Fitness and Individual Classes
creator.create('FitnessMax', base.Fitness, weights=(1.0,))
creator.create('Individual', list, fitness=creator.FitnessMax)
toolbox = base.Toolbox()


def get_near_time(p1, p2, start):
    try:
        travel_times = df_times[(df_times['p1'] == p1) & (df_times['p2'] == p2)]
        travel_times['start_time'] = travel_times['start_time'].apply(lambda x: int(x) - int(start))
        best_time = travel_times['start_time'].abs().sort_values()[0]
    except:
        best_time = 0
    return best_time


def genes_to_time(individual):
    """
    基因转换为消耗时间
    :param individual:
    :return:
    """
    global df_points, df_times
    error_routes = False
    total_time = 0
    # 检查时间窗口是否满足
    start_time = 0
    pre_index = None
    for index in individual:
        print('check index: ' + str(index))
        travel_time = 0
        # 排除预约时间有冲突的情况
        try:
            now_start = int(df_points.iloc[index]['start'])
        except:
            now_start = 0
        if start_time > 0 and now_start < start_time:
            error_routes = True
            break
        # 排除路程时间不够的情况
        if pre_index is not None:
            try:
                p1 = str(df_points.iloc[pre_index]['lon']) + ',' + str(df_points.iloc[pre_index]['lat'])
                p2 = str(df_points.iloc[index]['lon']) + ',' + str(df_points.iloc[index]['lat'])
                start = df_points.iloc[index]['start'] - 60
                interval_time = int(df_points.iloc[index]['start']) - int(df_points.iloc[pre_index]['stop'])
                travel_time = get_near_time(p1, p2, start)
            except:
                interval_time = 0
                travel_time = 0
            if travel_time > interval_time:
                error_routes = True
                break
        try:
            service_time = int(df_points.iloc[index]['stop']) - int(df_points.iloc[index]['start'])
        except:
            service_time = 0
        total_time += (travel_time + service_time)
        pre_index = index
        try:
            start_time = int(df_points.iloc[index]['start'])
        except:
            start_time = 0
    if error_routes:
        return None
    return total_time


def eval_vrptw(individual):
    """
    进化过程
    :param individual: 个体对象
    :param instance: 个体相关数据
    :return:
    """
    global df_points
    total_time = genes_to_time(individual)
    if total_time is not None:
        fitness = 1.0 / total_time
    else:
        fitness = 0
    return fitness,


def cx_partialy_matched(ind1, ind2):
    """
    变异方法
    Executes a partially matched crossover (PMX) on the input individuals.
    The two individuals are modified in place. This crossover expects
    :term:`sequence` individuals of indices, the result for any other type of
    individuals is unpredictable.

     This was the implmentation by the original author
     def cxPartialyMatched(ind1, ind2):
        size = min(len(ind1), len(ind2))
        cxpoint1, cxpoint2 = sorted(random.sample(range(size), 2))
        temp1 = ind1[cxpoint1:cxpoint2+1] + ind2
        temp2 = ind1[cxpoint1:cxpoint2+1] + ind1
        ind1 = []
        for x in temp1:
            if x not in ind1:
                ind1.append(x)
        ind2 = []
        for x in temp2:
            if x not in ind2:
                ind2.append(x)
        return ind1, ind2

    :param ind1: The first individual participating in the crossover.
    :param ind2: The second individual participating in the crossover.
    :returns: A tuple of two individuals.

    Moreover, this crossover generates two children by matching
    pairs of values in a certain range of the two parents and swapping the values
    of those indexes. For more details see [Goldberg1985]_.

    This function uses the :func:`~random.randint` function from the python base
    :mod:`random` module.

    .. [Goldberg1985] Goldberg and Lingel, "Alleles, loci, and the traveling
       salesman problem", 1985.

    Author: Paul
    Changes: decrease the index count by 1 to account for the way indivduals are
            generated with 0 being the terminal.

    """
    size = min(len(ind1), len(ind2))
    p1, p2 = [0] * size, [0] * size

    # Initialize the position of each indices in the individuals
    for i in range(size):
        p1[ind1[i] - 1] = i
        p2[ind2[i] - 1] = i
    # Choose crossover points
    cxpoint1 = random.randint(0, size)
    cxpoint2 = random.randint(0, size - 1)
    if cxpoint2 >= cxpoint1:
        cxpoint2 += 1
    else:  # Swap the two cx points
        cxpoint1, cxpoint2 = cxpoint2, cxpoint1

    # Apply crossover between cx points
    for i in range(cxpoint1, cxpoint2):
        # Keep track of the selected values
        temp1 = ind1[i]
        temp2 = ind2[i]
        # Swap the matched value
        ind1[i], ind1[p1[temp2 - 1]] = temp2, temp1
        ind2[i], ind2[p2[temp1 - 1]] = temp1, temp2
        # Position bookkeeping
        p1[temp1 - 1], p1[temp2 - 1] = p1[temp2 - 1], p1[temp1 - 1]
        p2[temp1 - 1], p2[temp2 - 1] = p2[temp2 - 1], p2[temp1 - 1]

    return ind1, ind2


def mut_inverse_indexes(individual):
    start, stop = sorted(random.sample(range(len(individual)), 2))
    individual = individual[:start] + individual[stop:start-1:-1] + individual[stop+1:]
    return individual,


def printRoute(route, merge=False, twoResources=False):
    routeStr = '0'
    subRouteCount = 0
    for subRoute in route:
        subRouteCount += 1
        subSubRouteCount = 0
        subRouteStr = '0'
        heavyRouteStr = '0'
        lightRouteStr = ''
        if twoResources:
            for subSubRoute in subRoute:
                subSubRouteCount += 1
                for customerID in subSubRoute:
                    # A lightSubRoute
                    if subSubRouteCount % 3 == 2:
                        lightRouteStr = lightRouteStr + ' - ' + str(customerID)
                        subRouteStr = subRouteStr + ' - ' + str(customerID)
                        routeStr = routeStr + ' - ' + str(customerID)
                    # A heavySubRoute
                    elif subSubRouteCount % 3 == 1 or subSubRouteCount % 3 == 0:
                        heavyRouteStr = heavyRouteStr + ' - ' + str(customerID)
                        subRouteStr = subRouteStr + ' - ' + str(customerID)
                        routeStr = routeStr + ' - ' + str(customerID)
                # Replace the lightSubRoute as a 'L' in heavySubRoute
                if subSubRouteCount % 3 == 2:
                    heavyRouteStr = heavyRouteStr + ' - L'
                # Replace the heavySubRoute as a 'H' in heavySubRoute
                elif subSubRouteCount % 3 == 1 or subSubRouteCount % 3 == 0:
                    lightRouteStr = lightRouteStr + ' - H'

            heavyRouteStr = heavyRouteStr + ' - 0'
            subRouteStr = subRouteStr + ' - 0'

            if not merge:
                print('  Vehicle %d\'s route: %s ' % (subRouteCount, subRouteStr))
                print('  Vehicle %d\'s heavy resource route %s ' % (subRouteCount, heavyRouteStr))
                print('  Vehicle %d\'s light resource route %s ' % (subRouteCount, lightRouteStr))


        else:
            for customerID in subRoute:
                subRouteStr = subRouteStr + ' - ' + str(customerID)
                routeStr = routeStr + ' - ' + str(customerID)
            subRouteStr = subRouteStr + ' - 0'
            if not merge:
                print('  Vehicle %d\'s route: %s' % (subRouteCount, subRouteStr))
                routeStr = routeStr + ' - 0'
        if merge:
            print(routeStr)
    return


def ga_vrptw(ind_size, pop_size, cxPb, mutPb, NGen, exportCSV=False):
    """
    遗传算法主程序
    :param ind_size: 基因大小
    :param popSize: 种群数量
    :param cxPb:
    :param mutPb:
    :param NGen:
    :param exportCSV:
    :return:
    """
    # Attribute generator
    toolbox.register('indexes', random.sample, range(1, ind_size + 1), ind_size)
    # Structure initializers
    toolbox.register('individual', tools.initIterate, creator.Individual, toolbox.indexes)
    # 生成初始的种群
    toolbox.register('population', tools.initRepeat, list, toolbox.individual)
    # Initialize the population
    pop = toolbox.population(n=pop_size)
    # Operator registering
    toolbox.register('evaluate', eval_vrptw)
    toolbox.register('select', tools.selRoulette)
    # toolbox.register('mate', cx_partialy_matched)
    # toolbox.register('mutate', mut_inverse_indexes)
    toolbox.register('mate', tools.cxPartialyMatched)
    toolbox.register('mutate', tools.mutShuffleIndexes)
    # Results holders for exporting results to CSV file
    csvData = []
    print('Start of evolution')
    # Evaluate the entire population
    fitnesses = list(toolbox.map(toolbox.evaluate, pop))
    for ind, fit in zip(pop, fitnesses):
        ind.fitness.values = fit
    # Debug, suppress print()
    # print('  Evaluated %d individuals' % len(pop)
    # Begin the evolution
    for g in range(NGen):
        print('-- Generation %d --' % g)
        # Select the next generation individuals
        # Select elite - the best offpsring, keep this past crossover/mutate
        elite = tools.selBest(pop, 1)
        # Select top 10% of all offspring
        # Roulette select the rest 90% of offsprings
        offspring = tools.selBest(pop, int(numpy.ceil(len(pop) * 0.1)))
        offspringRoulette = toolbox.select(pop, int(numpy.floor(len(pop) * 0.9)) - 1)
        offspring.extend(offspringRoulette)
        # Clone the selected individuals
        offspring = list(toolbox.map(toolbox.clone, offspring))
        # Apply crossover and mutation on the offspring
        for child1, child2 in zip(offspring[::2], offspring[1::2]):
            if random.random() < cxPb:
                toolbox.mate(child1, child2)
                del child1.fitness.values
                del child2.fitness.values
        for mutant in offspring:
            if random.random() < mutPb:
                toolbox.mutate(mutant)
                del mutant.fitness.values
        # Evaluate the individuals with an invalid fitness
        invalidInd = [ind for ind in offspring if not ind.fitness.valid]
        fitnesses = toolbox.map(toolbox.evaluate, invalidInd)
        for ind, fit in zip(invalidInd, fitnesses):
            ind.fitness.values = fit
        # Debug, suppress print()
        # print('  Evaluated %d individuals' % len(invalidInd)
        # The population is entirely replaced by the offspring
        # Debug, printing offspring
        offspring.extend(elite)
        pop[:] = offspring

        # Gather all the fitnesses in one list and print(the stats
        fits = [ind.fitness.values[0] for ind in pop]
        length = len(pop)
        mean = sum(fits) / length
        if mean == 0:
            mean = small_constant
        sum2 = sum(x * x for x in fits)
        std = abs(sum2 / length - mean ** 2) ** 0.5
        # Debug, suppress print()
        # print('  Min %s' % min(fits)
        # print('  Max %s' % max(fits)
        # print('  Avg %s' % mean
        # print('  Std %s' % std
        # Write data to holders for exporting results to CSV file
        if exportCSV:
            csvRow = {
                'generation': g,
                'evaluated_individuals': len(invalidInd),
                'min_fitness': min(fits),
                'max_fitness': max(fits),
                'avg_fitness': mean,
                'std_fitness': std,
                'avg_cost': 1 / mean,
            }
            csvData.append(csvRow)
    print('-- End of (successful) evolution --')
    bestInd = tools.selBest(pop, 1)[0]
    print('Best individual: %s' % bestInd)
    best_fitness = bestInd.fitness.values[0]
    if best_fitness == 0:
        best_fitness = small_constant
    print('Fitness: %s' % best_fitness)
    # printRoute(bestInd)
    # print(bestInd)
    print('Total cost: %s' % (1 / best_fitness))
    if exportCSV:
        csvFilename = 'pS%s_cP%s_mP%s_nG%s.csv' % (pop_size, cxPb, mutPb, NGen)
        csvPathname = os.path.join('results', csvFilename)
        print('Write to file: %s' % csvPathname)
        utils.makeDirsForFile(pathname=csvPathname)
        if not utils.exist(pathname=csvPathname, overwrite=True):
            with open(csvPathname, 'w') as f:
                fieldnames = ['generation', 'evaluated_individuals', 'min_fitness', 'max_fitness', 'avg_fitness',
                              'std_fitness', 'avg_cost']
                writer = DictWriter(f, fieldnames=fieldnames, dialect='excel')
                writer.writeheader()
                for csvRow in csvData:
                    writer.writerow(csvRow)
    return bestInd


def main():
    random.seed(73)
    indSize = len(df_points)
    popSize = 60000
    cxPb = 0.9
    mutPb = 0.05
    NGen = 2000
    exportCSV = True
    # Global creation of the individuals for GA
    ga_vrptw(
        ind_size=indSize,
        pop_size=popSize,
        cxPb=cxPb,
        mutPb=mutPb,
        NGen=NGen,
        exportCSV=exportCSV
    )


if __name__ == '__main__':
    if __package__ is None:
        import sys
        from os import path
        sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
        from gavrptw import utils
    else:
        from .gavrptw import utils
    pool = multiprocessing.Pool(4)
    toolbox.register("map", pool.map)
    tic = timer()
    main()
    print('Computing Time: %s' % (timer() - tic))
    pool.close()
